/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2017 Pentaho Corporation.. All rights reserved. */ package org.pentaho.plugin.jfreereport.reportcharts; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.CategoryAxis; import org.jfree.chart.axis.CategoryLabelPosition; import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.axis.CategoryLabelWidthType; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.DateTickUnit; import org.jfree.chart.axis.LogarithmicAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.NumberTickUnit; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.labels.ItemLabelAnchor; import org.jfree.chart.labels.ItemLabelPosition; import org.jfree.chart.labels.StandardCategoryItemLabelGenerator; import org.jfree.chart.plot.CategoryPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.CategoryItemRenderer; import org.jfree.data.category.CategoryDataset; import org.jfree.data.general.Dataset; import org.jfree.data.time.Day; import org.jfree.data.time.Hour; import org.jfree.data.time.Minute; import org.jfree.data.time.Month; import org.jfree.data.time.Second; import org.jfree.data.time.Year; import org.jfree.text.TextBlockAnchor; import org.jfree.ui.RectangleAnchor; import org.jfree.ui.TextAnchor; import org.pentaho.plugin.jfreereport.reportcharts.backport.FastNumberTickUnit; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.LegacyUpdateHandler; import org.pentaho.reporting.engine.classic.core.function.Expression; import org.pentaho.reporting.libraries.base.util.StringUtils; import org.pentaho.reporting.libraries.formatting.FastDecimalFormat; import java.awt.Font; import java.math.RoundingMode; import java.text.DateFormatSymbols; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.text.SimpleDateFormat; /** * This class allows you to embed categorical charts into JFreeReport XML definitions. * * @author mbatchel * @noinspection UnusedDeclaration */ public abstract class CategoricalChartExpression extends AbstractChartExpression implements LegacyUpdateHandler { private static final long serialVersionUID = -402500824047401239L; private static final double DEFAULT_SCALE_FACTOR = 1.0; private String valueAxisLabel; private String categoryAxisLabel; private boolean horizontal; private boolean showGridlines; private Double labelRotation; private Float maxCategoryLabelWidthRatio; private Font categoryTitleFont; private Font categoryTickFont; private String categoricalLabelFormat; private String categoricalLabelDecimalFormat; private String categoricalLabelDateFormat; private Double categoricalItemLabelRotation; private boolean humanReadableLogarithmicFormat; private boolean logarithmicAxis; private String categoricalAxisMessageFormat; private Font rangeTitleFont; private Font rangeTickFont; private double rangeMinimum; private double rangeMaximum; private boolean rangeIncludesZero; private boolean rangeStickyZero; private NumberFormat rangeTickFormat; private String rangeTickFormatString; private Class rangeTimePeriod; private double rangePeriodCount; private boolean autoRange; private double scaleFactor; private Double lowerMargin; private Double upperMargin; private Double categoryMargin; protected CategoricalChartExpression() { categoricalAxisMessageFormat = "{0}"; categoricalLabelFormat = "{2}"; rangeMaximum = 1; rangeMinimum = 0; showGridlines = true; rangePeriodCount = 0; autoRange = true; scaleFactor = DEFAULT_SCALE_FACTOR; } public Font getCategoryTitleFont() { return categoryTitleFont; } public void setCategoryTitleFont( final Font categoryTitleFont ) { this.categoryTitleFont = categoryTitleFont; } public Font getCategoryTickFont() { return categoryTickFont; } public void setCategoryTickFont( final Font categoryTickFont ) { this.categoryTickFont = categoryTickFont; } public String getRangeTickFormatString() { return rangeTickFormatString; } public void setRangeTickFormatString( final String rangeTickFormatString ) { this.rangeTickFormatString = rangeTickFormatString; } public String getCategoricalAxisMessageFormat() { return categoricalAxisMessageFormat; } public void setCategoricalAxisMessageFormat( final String categoricalAxisMessageFormat ) { this.categoricalAxisMessageFormat = categoricalAxisMessageFormat; } /** * Return the java.awt.Font to be used to display the range axis tick labels * * @return Font The Font for the range axis tick labels */ public Font getRangeTickFont() { return rangeTickFont; } /** * @param rangeTickFont The rangeTitleFont to set. */ public void setRangeTickFont( final Font rangeTickFont ) { this.rangeTickFont = rangeTickFont; } /** * Return the range axis' minimum value * * @return double Range axis' minimum value */ public double getRangeMinimum() { return rangeMinimum; } /** * @param rangeMinimum Set the minimum value of the range axis. */ public void setRangeMinimum( final double rangeMinimum ) { this.rangeMinimum = rangeMinimum; } /** * Return the range axis' maximum value * * @return double Range axis' maximum value */ public double getRangeMaximum() { return rangeMaximum; } /** * @param rangeMaximum Set the maximum value of the range axis. */ public void setRangeMaximum( final double rangeMaximum ) { this.rangeMaximum = rangeMaximum; } /** * @return Returns the rangeTitleFont. */ public Font getRangeTitleFont() { return rangeTitleFont; } /** * @param rangeTitleFont The rangeTitleFont to set. */ public void setRangeTitleFont( final Font rangeTitleFont ) { this.rangeTitleFont = rangeTitleFont; } /** * @return Returns the rangeTickFormat. */ public NumberFormat getRangeTickFormat() { return rangeTickFormat; } /** * @param rangeTickFormat The range tick number format to set. */ public void setRangeTickFormat( final NumberFormat rangeTickFormat ) { this.rangeTickFormat = rangeTickFormat; } /** * @return Returns the rangeIncludeZero. */ public boolean isRangeIncludesZero() { return rangeIncludesZero; } /** * @param rangeIncludesZero The domainIncludesZero to set. */ public void setRangeIncludesZero( final boolean rangeIncludesZero ) { this.rangeIncludesZero = rangeIncludesZero; } /** * @return Returns the rangeStickyZero. */ public boolean isRangeStickyZero() { return rangeStickyZero; } /** * @param rangeStickyZero The rangeStickyZero to set. */ public void setRangeStickyZero( final boolean rangeStickyZero ) { this.rangeStickyZero = rangeStickyZero; } public boolean isLogarithmicAxis() { return logarithmicAxis; } public void setLogarithmicAxis( final boolean logarithmicAxis ) { this.logarithmicAxis = logarithmicAxis; } public boolean isHumanReadableLogarithmicFormat() { return humanReadableLogarithmicFormat; } public void setHumanReadableLogarithmicFormat( final boolean humanReadableLogarithmicFormat ) { this.humanReadableLogarithmicFormat = humanReadableLogarithmicFormat; } public Double getLowerMargin() { return lowerMargin; } public void setLowerMargin( final Double lowerMargin ) { this.lowerMargin = lowerMargin; } public Double getUpperMargin() { return upperMargin; } public void setUpperMargin( final Double upperMargin ) { this.upperMargin = upperMargin; } public Double getCategoryMargin() { return categoryMargin; } public void setCategoryMargin( final Double categoryMargin ) { this.categoryMargin = categoryMargin; } public Double getLabelRotationDeg() { if ( labelRotation == null ) { return null; } else { return new Double( StrictMath.toDegrees( labelRotation.doubleValue() ) ); } } public void setLabelRotationDeg( final Double value ) { if ( value == null ) { labelRotation = null; } else { labelRotation = new Double( StrictMath.toRadians( value.doubleValue() ) ); } } public Double getLabelRotation() { return labelRotation; } public void setLabelRotation( final Double value ) { labelRotation = value; } public Double getCategoricalItemLabelRotationDeg() { if ( categoricalItemLabelRotation == null ) { return null; } else { return new Double( StrictMath.toDegrees( categoricalItemLabelRotation.doubleValue() ) ); } } public void setCategoricalItemLabelRotationDeg( final Double value ) { if ( value == null ) { categoricalItemLabelRotation = null; } else { categoricalItemLabelRotation = new Double( StrictMath.toRadians( value.doubleValue() ) ); } } public Double getCategoricalItemLabelRotation() { return this.categoricalItemLabelRotation; } public void setCategoricalItemLabelRotation( final Double value ) { this.categoricalItemLabelRotation = value; } public void setMaxCategoryLabelWidthRatio( final Float value ) { maxCategoryLabelWidthRatio = value; } public Float getMaxCategoryLabelWidthRatio() { return maxCategoryLabelWidthRatio; } public boolean isShowGridlines() { return showGridlines; } public void setShowGridlines( final boolean value ) { showGridlines = value; } public boolean isHorizontal() { return horizontal; } public void setHorizontal( final boolean value ) { horizontal = value; } public String getValueAxisLabel() { return valueAxisLabel; } public void setValueAxisLabel( final String valueAxisLabel ) { this.valueAxisLabel = valueAxisLabel; } public String getCategoryAxisLabel() { return categoryAxisLabel; } public void setCategoryAxisLabel( final String categoryAxisLabel ) { this.categoryAxisLabel = categoryAxisLabel; } public void setCategoricalLabelFormat( final String value ) { this.categoricalLabelFormat = value; } public String getCategoricalLabelFormat() { return this.categoricalLabelFormat; } public void setCategoricalLabelDecimalFormat( final String value ) { this.categoricalLabelDecimalFormat = value; } public String getCategoricalLabelDecimalFormat() { return this.categoricalLabelDecimalFormat; } public void setCategoricalLabelDateFormat( final String value ) { this.categoricalLabelDateFormat = value; } public String getCategoricalLabelDateFormat() { return this.categoricalLabelDateFormat; } public boolean isAutoRange() { return autoRange; } public void setAutoRange( final boolean autoRange ) { this.autoRange = autoRange; } public double getScaleFactor() { return scaleFactor; } public void setScaleFactor( final double scaleFactor ) { this.scaleFactor = scaleFactor; } protected JFreeChart computeChart( final Dataset dataset ) { if ( dataset instanceof CategoryDataset == false ) { return computeCategoryChart( null ); } final CategoryDataset categoryDataset = (CategoryDataset) dataset; return computeCategoryChart( categoryDataset ); } protected JFreeChart computeCategoryChart( final CategoryDataset dataset ) { return getChart( dataset ); } /** * @param categoryDataset the dataset. * @return the generated chart. This implementation returns null. * @deprecated should not be public and should not be a getter. In fact. it will be removed in PRD-4.0 */ public JFreeChart getChart( final CategoryDataset categoryDataset ) { return null; } protected PlotOrientation computePlotOrientation() { final PlotOrientation orientation; if ( isHorizontal() ) { orientation = PlotOrientation.HORIZONTAL; } else { orientation = PlotOrientation.VERTICAL; } return orientation; } protected void configureChart( final JFreeChart chart ) { super.configureChart( chart ); final CategoryPlot cpl = chart.getCategoryPlot(); final CategoryItemRenderer renderer = cpl.getRenderer(); if ( StringUtils.isEmpty( getTooltipFormula() ) == false ) { renderer.setBaseToolTipGenerator( new FormulaCategoryTooltipGenerator( getRuntime(), getTooltipFormula() ) ); } if ( StringUtils.isEmpty( getUrlFormula() ) == false ) { renderer.setBaseItemURLGenerator( new FormulaCategoryURLGenerator( getRuntime(), getUrlFormula() ) ); } if ( this.categoricalLabelFormat != null ) { final StandardCategoryItemLabelGenerator scilg; if ( categoricalLabelDecimalFormat != null ) { final DecimalFormat numFormat = new DecimalFormat( categoricalLabelDecimalFormat, new DecimalFormatSymbols( getRuntime().getResourceBundleFactory().getLocale() ) ); numFormat.setRoundingMode( RoundingMode.HALF_UP ); scilg = new StandardCategoryItemLabelGenerator( categoricalLabelFormat, numFormat ); } else if ( categoricalLabelDateFormat != null ) { scilg = new StandardCategoryItemLabelGenerator( categoricalLabelFormat, new SimpleDateFormat( categoricalLabelDateFormat, getRuntime().getResourceBundleFactory().getLocale() ) ); } else { final DecimalFormat formatter = new DecimalFormat(); formatter.setDecimalFormatSymbols( new DecimalFormatSymbols( getRuntime().getResourceBundleFactory().getLocale() ) ); scilg = new StandardCategoryItemLabelGenerator( categoricalLabelFormat, formatter ); } renderer.setBaseItemLabelGenerator( scilg ); } renderer.setBaseItemLabelsVisible( Boolean.TRUE.equals( getItemsLabelVisible() ) ); if ( getItemLabelFont() != null ) { renderer.setBaseItemLabelFont( getItemLabelFont() ); } if ( categoricalItemLabelRotation != null ) { final ItemLabelPosition orgPosItemLabelPos = renderer.getBasePositiveItemLabelPosition(); if ( orgPosItemLabelPos == null ) { final ItemLabelPosition pos2 = new ItemLabelPosition( ItemLabelAnchor.OUTSIDE12, TextAnchor.BOTTOM_CENTER, TextAnchor.CENTER, categoricalItemLabelRotation.doubleValue() ); renderer.setBasePositiveItemLabelPosition( pos2 ); } else { final ItemLabelPosition pos2 = new ItemLabelPosition( orgPosItemLabelPos.getItemLabelAnchor(), orgPosItemLabelPos.getTextAnchor(), orgPosItemLabelPos.getRotationAnchor(), categoricalItemLabelRotation.doubleValue() ); renderer.setBasePositiveItemLabelPosition( pos2 ); } final ItemLabelPosition orgNegItemLabelPos = renderer.getBaseNegativeItemLabelPosition(); if ( orgNegItemLabelPos == null ) { final ItemLabelPosition pos2 = new ItemLabelPosition( ItemLabelAnchor.OUTSIDE12, TextAnchor.BOTTOM_CENTER, TextAnchor.CENTER, categoricalItemLabelRotation.doubleValue() ); renderer.setBaseNegativeItemLabelPosition( pos2 ); } else { final ItemLabelPosition neg2 = new ItemLabelPosition( orgNegItemLabelPos.getItemLabelAnchor(), orgNegItemLabelPos.getTextAnchor(), orgNegItemLabelPos.getRotationAnchor(), categoricalItemLabelRotation.doubleValue() ); renderer.setBaseNegativeItemLabelPosition( neg2 ); } } final Font labelFont = Font.decode( getLabelFont() ); final CategoryAxis categoryAxis = cpl.getDomainAxis(); categoryAxis.setLabelFont( labelFont ); categoryAxis.setTickLabelFont( labelFont ); if ( getCategoryTitleFont() != null ) { categoryAxis.setLabelFont( getCategoryTitleFont() ); } if ( getCategoryTickFont() != null ) { categoryAxis.setTickLabelFont( getCategoryTickFont() ); } if ( maxCategoryLabelWidthRatio != null ) { categoryAxis.setMaximumCategoryLabelWidthRatio( maxCategoryLabelWidthRatio.floatValue() ); } cpl.setDomainGridlinesVisible( showGridlines ); if ( labelRotation != null ) { double angle = labelRotation.doubleValue(); CategoryLabelPosition top = createUpRotationCategoryLabelPosition( PlaneDirection.TOP, angle ); CategoryLabelPosition bottom = createUpRotationCategoryLabelPosition( PlaneDirection.BOTTOM, angle ); CategoryLabelPosition left = createUpRotationCategoryLabelPosition( PlaneDirection.LEFT, angle ); CategoryLabelPosition right = createUpRotationCategoryLabelPosition( PlaneDirection.RIGHT, angle ); CategoryLabelPositions rotationLabelPositions = new CategoryLabelPositions( top, bottom, left, right ); categoryAxis.setCategoryLabelPositions( rotationLabelPositions ); } final String[] colors = getSeriesColor(); for ( int i = 0; i < colors.length; i++ ) { renderer.setSeriesPaint( i, parseColorFromString( colors[i] ) ); } if ( lowerMargin != null ) { categoryAxis.setLowerMargin( lowerMargin.doubleValue() ); } if ( upperMargin != null ) { categoryAxis.setUpperMargin( upperMargin.doubleValue() ); } if ( categoryMargin != null ) { categoryAxis.setCategoryMargin( categoryMargin.doubleValue() ); } configureRangeAxis( cpl, labelFont ); } protected void configureRangeAxis( final CategoryPlot cpl, final Font labelFont ) { final ValueAxis rangeAxis = cpl.getRangeAxis(); if ( rangeAxis instanceof NumberAxis ) { final NumberAxis numberAxis = (NumberAxis) rangeAxis; numberAxis.setAutoRangeIncludesZero( isRangeIncludesZero() ); numberAxis.setAutoRangeStickyZero( isRangeStickyZero() ); if ( getRangePeriodCount() > 0 ) { if ( getRangeTickFormat() != null ) { numberAxis.setTickUnit( new NumberTickUnit( getRangePeriodCount(), getRangeTickFormat() ) ); } else if ( getRangeTickFormatString() != null ) { final FastDecimalFormat formatter = new FastDecimalFormat( getRangeTickFormatString(), getResourceBundleFactory().getLocale() ); numberAxis.setTickUnit( new FastNumberTickUnit( getRangePeriodCount(), formatter ) ); } else { numberAxis.setTickUnit( new FastNumberTickUnit( getRangePeriodCount() ) ); } } else { if ( getRangeTickFormat() != null ) { numberAxis.setNumberFormatOverride( getRangeTickFormat() ); } else if ( getRangeTickFormatString() != null ) { final DecimalFormat formatter = new DecimalFormat( getRangeTickFormatString(), new DecimalFormatSymbols( getResourceBundleFactory().getLocale() ) ); numberAxis.setNumberFormatOverride( formatter ); standardTickUnitsApplyFormat( numberAxis, formatter ); } } } else if ( rangeAxis instanceof DateAxis ) { final DateAxis numberAxis = (DateAxis) rangeAxis; if ( getRangePeriodCount() > 0 && getRangeTimePeriod() != null ) { if ( getRangeTickFormatString() != null ) { final SimpleDateFormat formatter = new SimpleDateFormat( getRangeTickFormatString(), new DateFormatSymbols( getResourceBundleFactory().getLocale() ) ); numberAxis.setTickUnit( new DateTickUnit( getDateUnitAsInt( getRangeTimePeriod() ), (int) getRangePeriodCount(), formatter ) ); } else { numberAxis.setTickUnit( new DateTickUnit( getDateUnitAsInt( getRangeTimePeriod() ), (int) getRangePeriodCount() ) ); } } else if ( getRangeTickFormatString() != null ) { final SimpleDateFormat formatter = new SimpleDateFormat( getRangeTickFormatString(), new DateFormatSymbols( getResourceBundleFactory().getLocale() ) ); numberAxis.setDateFormatOverride( formatter ); } } if ( rangeAxis != null ) { rangeAxis.setLabelFont( labelFont ); rangeAxis.setTickLabelFont( labelFont ); if ( getRangeTitleFont() != null ) { rangeAxis.setLabelFont( getRangeTitleFont() ); } if ( getRangeTickFont() != null ) { rangeAxis.setTickLabelFont( getRangeTickFont() ); } final int level = getRuntime().getProcessingContext().getCompatibilityLevel(); if ( ClassicEngineBoot.isEnforceCompatibilityFor( level, 3, 8 ) ) { if ( getRangeMinimum() != 0 ) { rangeAxis.setLowerBound( getRangeMinimum() ); } if ( getRangeMaximum() != 1 ) { rangeAxis.setUpperBound( getRangeMaximum() ); } if ( getRangeMinimum() == 0 && getRangeMaximum() == 0 ) { rangeAxis.setAutoRange( true ); } } else { if ( isAutoRange() ) { rangeAxis.setAutoRange( isAutoRange() ); } else { double factor = getScaleFactor(); if ( factor > DEFAULT_SCALE_FACTOR ) { // PRD-5340 hack // this method is invoked after all series were populated // hence the axis already has the graph's max and min values; double lower = rangeAxis.getLowerBound(); if ( lower < 0 ) { lower *= factor; } else if ( lower > 0 ) { lower /= factor; } double upper = rangeAxis.getUpperBound(); if ( upper > 0 ) { upper *= factor; } else if ( upper < 0 ) { upper /= factor; } rangeAxis.setRange( lower, upper ); } else { // the 'scaleFactor' property is left intact or has an incorrect value rangeAxis.setUpperBound( getRangeMaximum() ); rangeAxis.setLowerBound( getRangeMinimum() ); } } } } } protected void configureLogarithmicAxis( final CategoryPlot plot ) { if ( isLogarithmicAxis() ) { final LogarithmicAxis logarithmicAxis; if ( isHumanReadableLogarithmicFormat() ) { plot.getRenderer().setBaseItemLabelGenerator( new LogCategoryItemLabelGenerator() ); logarithmicAxis = new ScalingLogarithmicAxis( getValueAxisLabel() ); logarithmicAxis.setStrictValuesFlag( false ); } else { logarithmicAxis = new LogarithmicAxis( getValueAxisLabel() ); logarithmicAxis.setStrictValuesFlag( false ); } plot.setRangeAxis( logarithmicAxis ); } } public Class getRangeTimePeriod() { return rangeTimePeriod; } public void setRangeTimePeriod( final Class rangeTimePeriod ) { this.rangeTimePeriod = rangeTimePeriod; } public double getRangePeriodCount() { return rangePeriodCount; } public void setRangePeriodCount( final double rangePeriodCount ) { this.rangePeriodCount = rangePeriodCount; } /** * Return a completly separated copy of this function. The copy does no longer share any changeable objects with the * original function. * * @return a copy of this function. */ public Expression getInstance() { final CategoricalChartExpression expression = (CategoricalChartExpression) super.getInstance(); if ( expression.rangeTickFormat != null ) { expression.rangeTickFormat = (NumberFormat) expression.rangeTickFormat.clone(); } return expression; } protected int getDateUnitAsInt( final Class domainTimePeriod ) { if ( Second.class.equals( domainTimePeriod ) ) { return DateTickUnit.SECOND; } if ( Minute.class.equals( domainTimePeriod ) ) { return DateTickUnit.MINUTE; } if ( Hour.class.equals( domainTimePeriod ) ) { return DateTickUnit.HOUR; } if ( Day.class.equals( domainTimePeriod ) ) { return DateTickUnit.DAY; } if ( Month.class.equals( domainTimePeriod ) ) { return DateTickUnit.MONTH; } if ( Year.class.equals( domainTimePeriod ) ) { return DateTickUnit.YEAR; } if ( Second.class.equals( domainTimePeriod ) ) { return DateTickUnit.MILLISECOND; } return DateTickUnit.DAY; } public void reconfigureForCompatibility( final int versionTag ) { if ( ClassicEngineBoot.isEnforceCompatibilityFor( versionTag, 3, 8 ) ) { setAutoRange( getRangeMinimum() == 0 && getRangeMaximum() == 1 ); } } /** * Used instead of <code>org.jfree.chart.axis.CategoryLabelPosition.createUpRotationLabelPositions</code>. * <p> * It additionally takes into consideration the axis position. * * @param axisPosition * @param labelAngle * @return */ protected CategoryLabelPosition createUpRotationCategoryLabelPosition( PlaneDirection axisPosition, double labelAngle ) { RectangleAnchor categoryAnchor = axisPosition.opposite().asRectangleAnchor(); double labelAnchorDirectionAngle = axisPosition.opposite().asAngle() - labelAngle; PlaneDirection labelAnchorDirection = getTextAnchorDirectionOfAngle( labelAnchorDirectionAngle ); TextBlockAnchor labelAnchor = labelAnchorDirection.asTextBlockAnchor(); TextAnchor rotationAnchor = labelAnchorDirection.asTextAnchor(); return new CategoryLabelPosition( categoryAnchor, labelAnchor, rotationAnchor, -labelAngle, CategoryLabelWidthType.RANGE, 0.50f ); } /** * Chooses a proper anchor for a text label at a chart axis tick. * <p> * E.g. * <p> * Axis position is LEFT, label rotation = 0. So angle = 0. * <p> * Axis position is BOTTOM, label rotation = 90. So angle = 0. * <p> * Axis position is BOTTOM, label rotation = 0. So angle = pi/2 (90 degrees). * * @param angle can be assumed as the label-relative direction to the axis. * @return */ protected PlaneDirection getTextAnchorDirectionOfAngle( double angle ) { //Divide to 32 sectors (0..31). Counterclockwise from RIGHT. int sectorIndex = ( (int) ( ( ( ( angle * 16 / Math.PI ) ) % 32 ) + 32 ) ) % 32; switch ( sectorIndex ) { case 5: case 6: return PlaneDirection.TOP_RIGHT; case 7: case 8: return PlaneDirection.TOP; case 9: case 10: return PlaneDirection.TOP_LEFT; case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: return PlaneDirection.LEFT; case 21: case 22: return PlaneDirection.BOTTOM_LEFT; case 23: case 24: return PlaneDirection.BOTTOM; case 25: case 26: return PlaneDirection.BOTTOM_RIGHT; case 27: case 28: case 29: case 30: case 31: case 0: case 1: case 2: case 3: case 4: default: return PlaneDirection.RIGHT; } } /** * Local utility enum. * Used to calculate ahchors. */ static enum PlaneDirection { RIGHT, TOP_RIGHT, TOP, TOP_LEFT, LEFT, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT; private static final int COUNT = values().length; public static PlaneDirection byUnlimitedIndex( int unlimitedIndex ) { return values()[( unlimitedIndex % COUNT + COUNT ) % COUNT]; } public PlaneDirection opposite() { return byUnlimitedIndex( this.ordinal() + COUNT / 2 ); } public RectangleAnchor asRectangleAnchor() { switch ( this ) { case RIGHT: return RectangleAnchor.RIGHT; case TOP_RIGHT: return RectangleAnchor.TOP_RIGHT; case TOP: return RectangleAnchor.TOP; case TOP_LEFT: return RectangleAnchor.TOP_LEFT; case LEFT: return RectangleAnchor.LEFT; case BOTTOM_LEFT: return RectangleAnchor.BOTTOM_LEFT; case BOTTOM: return RectangleAnchor.BOTTOM; case BOTTOM_RIGHT: return RectangleAnchor.BOTTOM_RIGHT; default: return null; } } public TextBlockAnchor asTextBlockAnchor() { switch ( this ) { case RIGHT: return TextBlockAnchor.CENTER_RIGHT; case TOP_RIGHT: return TextBlockAnchor.TOP_RIGHT; case TOP: return TextBlockAnchor.TOP_CENTER; case TOP_LEFT: return TextBlockAnchor.TOP_LEFT; case LEFT: return TextBlockAnchor.CENTER_LEFT; case BOTTOM_LEFT: return TextBlockAnchor.BOTTOM_LEFT; case BOTTOM: return TextBlockAnchor.BOTTOM_CENTER; case BOTTOM_RIGHT: return TextBlockAnchor.BOTTOM_RIGHT; default: return null; } } public TextAnchor asTextAnchor() { switch ( this ) { case RIGHT: return TextAnchor.CENTER_RIGHT; case TOP_RIGHT: return TextAnchor.TOP_RIGHT; case TOP: return TextAnchor.TOP_CENTER; case TOP_LEFT: return TextAnchor.TOP_LEFT; case LEFT: return TextAnchor.CENTER_LEFT; case BOTTOM_LEFT: return TextAnchor.BOTTOM_LEFT; case BOTTOM: return TextAnchor.BOTTOM_CENTER; case BOTTOM_RIGHT: return TextAnchor.BOTTOM_RIGHT; default: return null; } } public double asAngle() { return this.ordinal() * 0.25 * Math.PI; } } }